//
//  TerminalListener.m
//  macapp
//
//  Created by Cocoa on 2020/5/27.
//  Copyright © 2020 Talent. All rights reserved.
//

#import "TerminalListener.h"
#include <sys/types.h>
#include <sys/event.h>
#include <sys/time.h>

@interface TerminalListener ()

@property (nonatomic, readonly, nullable) NSNumber *processIdentifier;
@property (nonatomic) BOOL watchedTermination;
@property (nonatomic, copy) void (^completionBlock)(BOOL);

@end

@implementation TerminalListener

- (instancetype)initWithProcessIdentifier:(NSNumber *)processIdentifier {
    self = [super init];
    if (self) {
        _processIdentifier = processIdentifier;
    }
    return self;
}

- (void)startListeningWithCompletion:(void (^)(BOOL))completionBlock {
    _completionBlock = completionBlock;
    
    if (self.processIdentifier == nil) {
        [self invokeCompletionWithSuccess:YES];
        return;
    }
    
    pid_t processIdentifier = self.processIdentifier.intValue;
    int queue = kqueue();
    if (queue == -1) {
        NSLog(@"Failed create kqueue()");
        [self invokeCompletionWithSuccess:NO];
        return;
    }
    
    struct kevent changes;
    EV_SET(&changes, processIdentifier, EVFILT_PROC, EV_ADD | EV_RECEIPT, NOTE_EXIT, 0, NULL);
    
    if (kevent(queue, &changes, 1, &changes, 1, NULL) == -1) {
        NSLog(@"Failed to invoke kevent() due to error %d: %@", errno, @(strerror(errno)));
        [self invokeCompletionWithSuccess:NO];
        return;
    }
    
    // We will assume this terminationListener will never be deallocated
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wcast-qual"
    CFFileDescriptorContext context = { 0, (void *)CFBridgingRetain(self), NULL, NULL, NULL };
#pragma clang diagnostic pop
    CFFileDescriptorRef noteExitKQueueRef = CFFileDescriptorCreate(NULL, queue, true, noteExitKQueueCallback, &context);
    if (noteExitKQueueRef == NULL) {
        NSLog(@"Failed to create file descriptor via CFFileDescriptorCreate()");
        CFRelease((__bridge CFTypeRef)(self));
        [self invokeCompletionWithSuccess:NO];
        return;
    }
    
    CFRunLoopSourceRef runLoopSource = CFFileDescriptorCreateRunLoopSource(NULL, noteExitKQueueRef, 0);
    if (runLoopSource == NULL) {
        NSLog(@"Failed to create runLoopSource via CFFileDescriptorCreateRunLoopSource()");
        CFRelease((__bridge CFTypeRef)(self));
        [self invokeCompletionWithSuccess:NO];
        return;
    }
    
    CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, kCFRunLoopDefaultMode);
    CFRelease(runLoopSource);
    
    CFFileDescriptorEnableCallBacks(noteExitKQueueRef, kCFFileDescriptorReadCallBack);
    
    // Make sure we didn't set the listener callback to a dead PID
    // If we did, we could hang forever. To avoid this, we check if the process has terminated *after* we set up the callback
    // If we tried to do this check before setting the callback, we could run into an issue where the process can terminate after our check
    // but before setting the callback
    if ([self terminated]) {
        [self invokeCompletionWithSuccess:YES];
    }
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        if ([self terminated]) {
            [self invokeCompletionWithSuccess:YES];
        }
    });
    
}

#pragma mark - Pivate

- (void)invokeCompletionWithSuccess:(BOOL)success {
    if (self.completionBlock != nil) {
        self.completionBlock(success);
        self.completionBlock = nil;
    }
}

- (BOOL)terminated {
    return (self.watchedTermination || self.processIdentifier == nil) ? YES : (kill(self.processIdentifier.intValue, 0) != 0);
}



static void noteExitKQueueCallback(CFFileDescriptorRef file, CFOptionFlags __unused callBackTypes, void *info) {
    struct kevent event;
    kevent(CFFileDescriptorGetNativeDescriptor(file), NULL, 0, &event, 1, NULL);
    
    TerminalListener *self = CFBridgingRelease(info);
    self.watchedTermination = YES;
    [self invokeCompletionWithSuccess:YES];
}


@end
